from concepts import *


class DialogError(Exception):
    pass


class Dialog(Object):
    controller = Attribute(initiable = False, writable = True)
    primitive  = controller._.primitive
    screen     = controller._.screen
    
    elements = DictAttribute()
        
    @property
    def all_elements(self):
        return self.elements.values()
    
    @property
    def visible_elements(self):
        for element in self.all_elements:
            if element.renderer.visible:
                yield element
    
    def add_element(self, name, element):
        self._elements[name] = element
        self.__dict__ [name] = element
    
    def setup_elements(self):
        pass
    
    def setup_primitives(self):
        pass
    
    def setup_alignments(self):
        pass
    
    def setup_dialog(self):
        pass
    
    def open_event(self):
        self.setup_elements()
        self.setup_primitives()
        self.setup_alignments()
        self.setup_dialog()
        self.on_open()
    
    def close_event(self):
        self.on_close()
    
    def popup_event(self, popup):
        self.on_popop(popup)
    
    def update(self):
        self.on_update()
        self.update_elements()
        self.on_update_renderer()
    
    def update_elements(self):
        self.on_update_elements()
        
    def on_open(self):
        pass
    
    def on_close(self):
        pass
    
    def on_popop(self, popup):
        pass
    
    def on_update(self):
        pass
    
    def on_update_elements(self):
        for element in self.all_elements:
            element.update()
    
    def on_update_renderer(self):
        pass



class DialogController(Object):
    dialogs = ListAttribute(initiable = False)
    
    @property
    def current_dialog(self):
        'The top dialog of the dialog stack, or None.'
        
        if self.dialogs:
            return self.dialogs[-1]
        else:
            return None
    
    def dialogs_above(self, dialog):
        'All dialogs above this one, or the empty list.'
        
        if dialog not in self.dialogs:
            return []
        
        position = self.dialogs.index(dialog)
        
        return self.dialogs[(position + 1):]
    
    def set_dialog(self, dialog):
        'Replaces the entire dialog stack with the given dialog.'
        
        self.close_all_dialogs()
        self.add_dialog(dialog)
    
    def add_dialog(self, dialog):
        'Adds a dialog onto the stack. The dialog must not already be on the stack.'
        
        if dialog is None:
            return
        
        if dialog in self.dialogs:
            raise DialogError('A dialog cannot be on the stack twice')
        
        if dialog.controller is not None:
            raise DialogError('A dialog cannot be attached to more than one controller')
        
        dialog.controller = self
        
        if self.current_dialog is not None:
            self.dialog.popup_event(dialog)
        
        self._dialogs.append(dialog)
        
        dialog.open_event()
    
    def close_dialog(self, dialog):
        'Closes a dialog on the stack and all dialogs above it.'
        
        if dialog is None:
            return
        
        if dialog not in self.dialogs:
            raise DialogError('Cannot close a dialog that is not open')
        
        for _ in len(self.dialogs_above(dialog)) + 1:
            self.close_topmost_dialog()
    
    def close_all_dialogs(self):
        'Closes all dialogs.'
        
        while self.current_dialog is not None:
            self.close_topmost_dialog()
    
    def close_topmost_dialog(self):
        'Closes the dialog on the top of the stack.'
        
        if self.current_dialog is None:
            raise DialogError('Cannot close topmost dialog, none open')
        
        self.dialog.close_event()        
        self._dialogs.pop()
        
        dialog.controller = None

    def update_dialogs(self):
        'Sends an update event to all open dialogs, starting with the bottom-most.'
        
        for dialog in self.dialogs:
            dialog.update()
    
    @property
    def all_elements(self):
        'All elements in all dialogs'
        
        return flattened(list(dialog.all_elements) for dialog in self.dialogs)
    
    @property
    def visible_elements(self):
        'All visible elements in all dialogs'
        
        return flattened(list(dialog.visible_elements) for dialog in self.dialogs)
